1. 术语
- 连接点(JointPoint):代码中具有边界性质特定点;Spring仅支持方法的连接点,包含方法和方位两方面信息
- 切点(Pointcut):定位到某个方法
- 增强(Advice):织入到目标连接点上的代码
- 目标对象(Target):增强逻辑的目标织入类
- 引介(Introduction):特殊的增强,为类添加一些属性和方法
- 织入(Weaving):将增强添加到目标连接点上的过程:编译期织入、类装载期织入、动态代理织入(Spring的方案)
- 代理(Proxy):被AOP织入增强后的结果类
- 切面(Aspect):切点+增强
2. 动态代理的两种实现:JDK和CGLib
- JDK动态代理动态创建一个符合某一接口的实力,生成目标类的代理对象,缺点是需要提供接口;方法必须是
public
或public final
的 - CGLib采用底层的字节码技术,在子类中对父类的方法进行拦截,织入横切逻辑;不能为
final
和private
方法代理 - 样例
1 | package test; |
- 结果
1 | before process jdk proxy |
- 性能:CGLib所创建的动态代理对象性能比JDK方式高(约10倍),但CGLib在创建代理对象时所花费的时间比JDK方式多(约8倍);CGLib适合Spring里singleton模式的bean管理
3. ProxyFactory
- Spring定义了
org.springframework.aop.framework.AopProxy
接口及Cglib2AopProxy
和JdkDynamicAopProxy
两个final
实现类 - 如果通过
ProxyFactory
的setInterfaces(Class[] interfaces)
指定针对接口代理,则使用JdkDynamicAopProxy
;如果使用setOptimize(true)
,使用Cglib2AopProxy
ProxyFacotry
通过addAdvice(Advice)
形成增强链
4. 增强类型
4.1 前置增强
- 接口:
org.springframework.aop.BeforeAdvice
- 样例
1 | package com.aop; |
4.2 后置增强
- 接口:
org.springframework.aop.AfterReturninigAdvice
- 样例
1 | package com.aop; |
4.3 环绕增强
- 接口:
org.aopalliance.intercept.MethodInterceptor
- 样例
1 | package com.aop; |
4.4 异常抛出增强
- 接口:
org.springframework.aop.ThrowsAdvice
- 样例
1 | package com.aop; |
4.5 测试
4.5.1 基于代码的测试
TestAopAdvice
1 | package com.aop; |
- 输出
1 | before advice blabla |
4.5.2 基于Spring配置的测试
springAop.xml
1 | <?xml version="1.0" encoding="UTF-8"?> |
TestAopAdvice2
1 | package com.aop; |
- 输出
1 | 二月 09, 2017 9:54:11 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh |
4.6 引介增强
- 接口:
org.springframework.aop.IntroductionInterceptor
4.6.1 基于Spring配置的测试代码
IntroductionAdvice
1 | package com.aop; |
ConfigurableIntroduction
1 | package com.aop; |
TestIntroductionAdvice
1 | package com.aop; |
springAop.xml
添加
1 | <bean id="configurableIntroduction" class="com.aop.ConfigurableIntroduction" /> |
- 输出
1 | 二月 09, 2017 9:56:10 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh |
4.6.2 与其他增强在配置上的区别
- 须指定引介增强所实现的接口
- 只能通过为目标类创建子类的方式生成引介增强的代理,因此
proxyTargeClass
必须为true
5. Spring中的配置
参数说明
target
:代理的对象proxyInterfaces
:代理所要实现的接口interceptorNames
:需要织入目标对象的bean列表,这些bean必须是实现了org.aopalliance.intercept.MethodInterceptor
或org.springframework.aop.Advisor
的bean,配置中的顺序对应调用的顺序singleton
:返回的代理是否为单例,默认为true
optimize
:为true
时使用CGLib代理proxyTargetClass
:为true
时使用CGLib代理,并覆盖proxyInterfaces
设置
6. Java注解
- 一个例子
1 | package com.aspectj; |
- 成员以无入参无抛出异常的方式声明
- 可以通过
default
为成员指定一个默认值 - 在方法上使用注解:
@Authority(value=true)
- 如果注解只有一个成员,需命名为
value()
,使用时可以忽略成员名和赋值号(=),如@Authority(true)
- 注解类拥有多个成员时,如果仅对
value
成员赋值,可以不适用赋值号;如果同时对多个成员赋值,则必须使用赋值号 - 注解类可以没有成员,称为标识注解
- 所有注解类隐式继承于
java.lang.annotation.Annotation
,注解不允许显式继承于其他接口 - 如果成员是数组类型,可以通过{}赋值
7. 基于AspectJ的AOP
7.1 一个例子
- 定义切面
1 | package com.aspectj; |
- 测试
1 | package com.aspectj; |
- 结果
1 | aspect: before processing |
7.2 通过配置使用切面
7.2.1 典型配置
springAspectj.xml
1 | <?xml version="1.0" encoding="UTF-8"?> |
7.2.2 基于Schema的配置
springAspectj.xml
1 | <?xml version="1.0" encoding="UTF-8"?> |
AspectJTest2
1 | package com.aspectj; |
- 输出
1 | 二月 09, 2017 10:13:56 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh |
- 通过
<aop:aspectj-autoproxy />
引入aop
命名空间,自动为Spring容器中匹配@AspectJ
切面的bean创建代理,完成切面织入,其内部实现仍为AnnotationAwareAspectJAutoProxyCreator
<aop:aspectj-autoproxy />
的proxy-target-class
属性为false
时,采用JDK动态代理;为true
时使用CGLib
8. AspectJ语法
8.1 切点表达式函数
- 分类
类型 | 说明 | 举例 |
---|---|---|
方法切点函数 | 通过描述目标类方法信息定义连接点 | execution() ,@annotation() |
方法入参切点函数 | 通过描述目标类方法入参的信息定义连接点 | args() ,@args() |
目标类切点函数 | 通过描述目标类类型信息定义连接点 | within() ,target() ,@within() ,@target() |
代理类切点函数 | 通过描述目标类的代理类的信息定义连接点 | this() |
- 函数说明
函数 | 入参 | 说明 |
---|---|---|
execution() |
方法匹配模式串 | 表示满足某一匹配模式的所有目标类方法连接点,如execution(* handle(..)) 表示所有目标类中的handle() 方法 |
@annotation() |
方法注解类名 | 表示标注了特定注解的目标方法连接点,如@annotation(com.aspectj.Authority) 表示任何标注了@Authority 注解的目标类方法 |
args() |
类名 | 通过判别目标类方法运行时入参对象的类型定义指定连接点,如args(com.data.Car) 表示所有有且仅有一个按类型匹配于Car (含子类)入参的方法 |
@args() |
类型注解类名 | 通过判别目标方法运行时入参对象的类是否标注特定注解来制定连接点,如@args(com.aspectj.Authority) 表示任何这样的一个目标方法:它有一个入参且入参对象的类标注@Authority 注解。要使@args() 生效,类继承树中,标注注解的类类型需要不高于入参类类型 |
within |
类名匹配串 | 表示特定域下的所有连接点,如within(com.service.*) ,within(com.service.*Service) 和within(com.service..*) |
target() |
类名 | 假如目标按类型匹配于指定类,则目标类的所有连接点匹配这个切点。如通过target(com.data.Car) 定义的切点,Car 及Car 的子类中的所有连接点都匹配该切点,包括子类中扩展的方法 |
@within() |
类型注解类名 | 假如目标类按类型匹配于某个类A ,且类A 标注了特定注解,则目标类的所有连接点都匹配于这个切点。如@within(com.aspectj.Authority) 定义的切点,假如Car 类标注了@Authority 注解,则Car 以及Car 的子类的所有连接点都匹配。@within 标注接口类无效 |
@target() |
类型注解类名 | 目标类标注了特定注解,则目标类(不包括子类)所有连接点都匹配该切点。如通过@target(com.aspectj.Authority) 定义的切点,若BMWCar 标注了@Authority ,则BMWCar 所有连接点匹配该切点 |
this() |
类名 | 代理类按类型匹配于指定类,则被代理的目标类所有连接点匹配切点 |
8.2 通配符
8.2.1 通配符类型
类型 | 说明 |
---|---|
* |
匹配任意字符,但只能匹配上下文中的一个元素 |
.. |
匹配任意字符,可以匹配上下文中的多个元素。表示类时,和* 联合使用;表示入参时单独使用 |
+ |
按类型匹配指定类的所有类(包括实现类和继承类),必须跟在类名后面 |
8.2.1 函数按通配符支持分类
- 支持所有通配符:
execution()
,within()
- 仅支持
+
通配符:args()
,this()
,target()
- 不支持通配符:
@args
,@within
,@target
,@annotation
8.3 增强类型
@Before
:前置增强,相当于BeforeAdvice
@AfterReturning
:后置增强,相当于AfterReturningAdvice
@Around
:环绕增强,相当于MethodInterceptor
@AfterThrowing
:相当于ThrowsAdvice
@After
:Final增强,抛出异常或正常退出都会执行的增强@DeclareParents
:引介增强,相当于IntroductionInterceptor
8.4 Execution()
- 语法:
execution(<修饰符模式>? <返回类型模式> <方法名模式> (<参数模式>) <异常模式>?)
8.4.1 通过方法签名定义切点
execution(pulic * *(..))
:匹配目标类的public
方法,第一个*
代表返回类型,第二个*
代表方法名,..
代表任意入参execution(* *To(..))
:匹配目标类所有以To
结尾的方法,第一个*
代表返回类型,*To
代表任意以To
结尾的方法
8.4.2 通过类定义切点
execution(* com.data.User.*(..))
:匹配User
接口的所有方法execution(* com.data.User+.*(..))
:匹配User
接口的所有方法,包括其实现类中不在User
接口中定义的方法
8.4.3 通过类包定义切点
execution(* com.data.*(..))
:匹配data
包下所有类的所有方法execution(* com.data.User..*(..))
:匹配data
包及其子孙包中的所有类的所有方法execution(* com..*Manager.get*(..))
:匹配com
包及其子孙包中后缀为Manager
的类里以get
开头的方法
8.4.4 通过方法入参定义切点
execution(* get(String, int))
:匹配get(String, int)
方法execution(* get(String, *))
:匹配名为get且第一个入参类型为String
、第二个入参类型任意的方法execution(* get(String, ..))
:匹配名为get
且第一个入参为String
类型的方法execution(* get(Object+))
:匹配名为get
且唯一入参是Object
或其子类的方法
8.5 进阶
8.5.1 逻辑运算符
- 与
&&
,或||
,非!
8.5.2 切点复合运算
- 例如:
@After("within(com.data.*) && execution(* handle(..))")
8.5.3 命名切点
- 使用
@Pointcut
命名切点 - 使用方法名作为切点的名称,方法的访问控制符控制切点的可用性
1 | "within(com.data.*)") ( |
8.5.4 增强织入的顺序
- 如果增强在同一个切面类中声明,则依照增强在切面类中定义的顺序织入
- 如果增强位于不同的增强类中,且都实现了
org.springframework.core.Ordered
接口,则由接口方法的顺序号决定(顺序号小的先织入) - 如果增强位于不同的增强类中,且没有实现
org.springframework.core.Ordered
接口,织入顺序不确定
8.5.5 访问连接点信息
- AspectJ使用
org.aspectj.lang.JointPoint
接口表示目标类连接点对象。如果是环绕增强时,使用org.aspectj.lang.ProceedingJointPoint
表示连接点对象,该类是JointPoint
接口的子接口。任何一个增强方法都可以通过将第一个入参声明为JointPoint
访问到连接点上下文的信息
8.5.6 绑定连接点方法入参
args()
用于绑定连接点方法的入参,@annotation()
用于绑定连接点方法的注解对象,@args()
用于绑定连接点方法的入参注解。下例表示方法入参为(String, int, ..)
的方法匹配该切点,并将name
和age
两个参数绑定到切面方法的入参中
1 | "args(name, age, ..)") ( |
8.5.7 绑定代理对象
- 使用
this()
或target()
可以绑定被代理对象的实例。下例表示代理对象为User
类的所有方法匹配该切点,且代理对象绑定到user
入参中
1 | "this(user)") ( |
8.5.8 绑定类注解对象
@within()
和@target()
函数可以将目标类的注解对象绑定到增强方法中
1 | "@within(a)") ( |
8.5.9 绑定返回值
- 通过
returning
绑定连接点方法的返回值
1 | "target(com.data.Car)", returning="rvl") (value= |
rvl
的类型必须和连接点方法的返回值类型匹配
8.5.10 绑定抛出的异常
- 使用
AfterThrowing
注解的throwing
成员绑定
1 | "target(com.data.Car)", throwing="iae") (value= |